第30天了。好想來曬娃xd
這段日子真的是累得半死,沒錯!我發現挪時間讀書真的是累到爆,不開玩笑,特別是兒子生病、自己生病、半夜泡奶、有人沒睡過夜...暈
但某方面來說,這又像是另一個睡前的動力。謝謝一路看到這邊的各位。你們也很不簡單!(鞠躬
Day 29 最後留下這段:
async function loadData() {
const response = await fetch('/api/data')
return response.json()
}
它讀起來很像:
先執行 fetch
↓
停在 await
↓
資料回來
↓
再往下跑
以前我看到 await,會很自然把它理解成:
JavaScript 在這一行停下來等。
但如果主執行緒真的整個停住,網頁不就不能處理點擊、不能更新畫面,其他程式也不能跑?
所以最後一天想把前面幾天全部收回來,只問一個問題:
async/await 明明寫得像同步流程,為什麼 JavaScript 主執行緒沒有被卡住?
async function run() {
console.log('A')
await Promise.resolve()
console.log('B')
}
run()
console.log('C')
如果 await 是「整個 JavaScript 停住」,我可能會猜:
A
B
C
但一般情況下會得到:
A
C
B
這個順序其實已經把答案露出來了。
run() 開始
↓
印 A
↓
遇到 await
↓
run() 暫時交還控制權
↓
外面繼續跑
↓
印 C
↓
之後再回到 run()
↓
印 B
所以 await 暫停的不是整個 JavaScript 世界。
比較接近:
它暫停的是目前這個 async function 後面的執行,讓主執行緒先去做其他工作。
先看:
async function getNumber() {
return 42
}
const result = getNumber()
console.log(result)
result 不是直接的 42。
它是一個 Promise。
概念上可以先想成:
async function
↓
一定用 Promise 表示它未來的完成結果
所以:
getNumber().then(value => {
console.log(value)
})
最後會拿到:
42
這很重要,因為 async/await 並不是另一套跟 Promise 無關的非同步系統。
它其實是建立在 Promise 心智模型上。
看:
async function run() {
const value = await Promise.resolve(123)
console.log(value)
}
await 後面會先取得一個值。
如果那是一個 Promise,就等待它完成。
完成之後:
Promise fulfilled
↓
async function 剩下的部分
↓
之後再被安排回來執行
所以可以先把:
const value = await somePromise
console.log(value)
粗略理解成:
somePromise.then(value => {
console.log(value)
})
這不是要說兩種寫法在所有語意細節上都完全一樣。
而是建立心智模型:
await 後面的程式,本質上是在延續一個 Promise 完成後的流程。
回到:
async function run() {
console.log('A')
await Promise.resolve()
console.log('B')
}
run()
console.log('C')
可以拆成:
目前 Task 開始
↓
呼叫 run()
↓
印 A
↓
遇到 await
↓
把 run() 後半段暫停
↓
回到外層
↓
印 C
↓
目前同步程式結束
↓
處理 Promise 相關 Microtask
↓
恢復 run()
↓
印 B
所以結果是:
A
C
B
這就直接接回 Day 29:
Promise 的後續工作會透過 Microtask 排程。
async/await 把這種流程寫得比較像一般從上往下的程式。
看這段:
async function test() {
console.log('A')
const value = await 123
console.log(value)
console.log('B')
}
test()
console.log('C')
很多人會想:
123 又不是 Promise,應該不用等吧?
但執行順序仍會是:
A
C
123
B
原因不是 123 真的需要花時間取得。
而是 await 會把後續執行切開。
可以先想成:
await 123
↓
把 123 當成已經完成的結果
↓
但 async function 後半段仍在之後的排程點繼續
所以 await 不只是在「網路真的很慢」時才產生非同步邊界。
現在回到真實前端:
async function loadUser() {
const response = await fetch('/api/user')
const user = await response.json()
console.log(user)
}
第一個 await:
發出 fetch
↓
網路等待交給瀏覽器執行環境
↓
目前 async function 暫停
↓
JavaScript 可以先做別的事
↓
HTTP response 回來
↓
fetch 的 Promise 完成
↓
async function 有機會繼續
第二個 await:
response.json()
↓
回傳 Promise
↓
等待 Body 被讀取並解析
↓
完成後再繼續
所以它看起來像:
第一步做完
第二步做完
第三步做完
但底下其實仍然是一連串 Promise 與排程。
非同步在 Promise、callback 時代就已經存在。
async/await 解決的比較像:
讓依序相依的非同步流程,更接近人閱讀同步程式的方式。
例如 Promise chain:
fetch('/api/user')
.then(response => response.json())
.then(user => {
console.log(user)
})
.catch(error => {
console.error(error)
})
用 async/await:
async function loadUser() {
try {
const response = await fetch('/api/user')
const user = await response.json()
console.log(user)
} catch (error) {
console.error(error)
}
}
底層仍然不是同步 I/O。
只是控制流程變得比較容易由上往下閱讀。
正常等待 Promise 時,不會因為 await 本身卡住整個主執行緒。
真正會卡主執行緒的,是同步工作一直不結束。
例如:
console.log('start')
const start = Date.now()
while (Date.now() - start < 5000) {
// 故意同步忙 5 秒
}
console.log('end')
這五秒裡:
Call Stack 一直被占著
↓
事件不能正常處理
↓
Microtask 也沒有機會執行
↓
畫面更新也會被拖住
這跟:
await fetch('/api/data')
完全是不同的事情。
await 是把控制權交回去。
busy loop 是死抓著主執行緒不放。
假設:
async function loadData() {
console.log('開始抓資料')
const response = await fetch('/api/data')
console.log('資料回來了')
return response.json()
}
loadData()
console.log('使用者還可以繼續操作其他流程')
流程比較像:
JavaScript
↓
發起網路要求
↓
loadData 暫停
↓
主執行緒繼續跑其他 JavaScript
↓
瀏覽器繼續處理事件
↓
未來 fetch 完成
↓
Promise reaction 進入排程
↓
loadData 繼續
這也是為什麼「等待網路」跟「CPU 忙到爆」不能混為一談。
這裡也是實務很常踩的地方。
假設兩個請求彼此沒有依賴:
const user = await fetch('/api/user')
const news = await fetch('/api/news')
這種寫法會先等第一個完成,才開始第二個。
如果兩個本來可以同時開始,就可能浪費時間。
可以改成:
const userPromise = fetch('/api/user')
const newsPromise = fetch('/api/news')
const [user, news] = await Promise.all([
userPromise,
newsPromise
])
現在變成:
user request 先發出去
news request 也立刻發出去
↓
兩個一起等待
↓
全部完成後再往下
所以 async/await 雖然好讀,還是要知道自己到底在排什麼。
不然很容易把可以平行等待的事情,寫成一條很長的瀑布。
Promise 寫法常看到:
doSomething()
.then(...)
.catch(error => {
console.error(error)
})
async/await 則可以寫:
async function run() {
try {
const result = await doSomething()
console.log(result)
} catch (error) {
console.error(error)
}
}
如果 await 的 Promise rejected:
Promise rejected
↓
await 這個位置像是丟出一個錯誤
↓
附近的 try/catch 可以接住
所以 async/await 讓非同步錯誤處理也比較接近一般同步控制流程。
但要記得:
async function 自己如果沒有把錯誤 catch 起來,它回傳的 Promise 就會 rejected。
在瀏覽器 Console 跑:
async function demo() {
console.log('1')
await Promise.resolve()
console.log('2')
await Promise.resolve()
console.log('3')
}
console.log('A')
demo()
console.log('B')
先猜順序。
答案通常是:
A
1
B
2
3
第一個 await 讓 demo 暫時停下。
外層先印 B。
接著 demo 在 Microtask 中恢復,印 2。
又遇到第二個 await,再把後半段排到之後的 Microtask。
最後印 3。
async function demo() {
console.log('A')
await Promise.resolve()
console.log('B')
}
setTimeout(() => {
console.log('C')
}, 0)
demo()
console.log('D')
順序通常是:
A
D
B
C
因為:
A、D
→ 目前同步程式
B
→ await 後續的 Promise / Microtask
C
→ 下一個 Task
Day 28、29、30 在這裡就接起來了。
async function run() {
console.log('1')
await Promise.resolve()
console.log('2')
setTimeout(() => {
console.log('3')
}, 0)
await Promise.resolve()
console.log('4')
}
console.log('A')
run()
console.log('B')
順序是什麼?
A
1
B
2
4
3
先跑目前同步程式:
A
1
B
第一個 await 的後續成為 Microtask,所以接著印 2。
2 執行時建立 setTimeout,callback 會等下一個 Task。
接著又遇到 await,4 的後續成為新的 Microtask。
Microtask 還沒清空,所以先印 4。
最後才輪到 timeout 的 3。
JavaScript 開始執行一個 Task
↓
同步程式進入 Call Stack
↓
setTimeout / fetch 等等待工作
可以交給 Runtime
↓
Promise 完成後
相關 continuation 進 Microtask
↓
async function 遇到 await
暫停自己的後半段
↓
目前同步工作結束
↓
清 Microtask Queue
↓
async function 可能在這裡恢復
↓
下一個 Task
↓
setTimeout callback 等工作再進來
現在再看到:
await fetch(...)
我不會再把它理解成:
JavaScript 停住等網路。
而會理解成:
這個 async function 先停在這裡,等 Promise 完成;主執行緒可以先去做其他事,之後再透過排程回來繼續。
await 暫停的是目前 async function 的後續流程,不是把整個 JavaScript 主執行緒凍住。async/await 是 Promise 控制流程的語法抽象,Promise 完成後,函式後半段才在之後的排程點繼續。
所以現在看到非同步程式,我會問:
現在同步跑到哪裡?
↓
哪個工作交給 Runtime?
↓
哪個 Promise 還沒完成?
↓
await 暫停的是哪一個 async function?
↓
恢復時進的是 Microtask 還是下一個 Task?
Day 01 一開始只是:
npm run dev
老師叫我打,我就打。
一路往下追,才發現一個看起來很普通的前端專案,後面其實接著很多層:
npm
↓
package.json
↓
node_modules
↓
Vite
↓
App.jsx
↓
Component
↓
Array / Object / props
↓
state
↓
function
↓
scope
↓
hoisting
↓
closure
↓
this
↓
Call Stack
↓
Event Loop
↓
Promise
↓
Microtask
↓
async / await
完。